package study.alex.resumedb.gae.model;

import com.google.api.client.auth.oauth2.Credential;
import com.google.api.client.auth.oauth2.TokenResponseException;
import com.google.api.client.extensions.appengine.http.UrlFetchTransport;
import com.google.api.client.googleapis.json.GoogleJsonResponseException;
import com.google.api.client.http.AbstractInputStreamContent;
import com.google.api.client.http.ByteArrayContent;
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpResponse;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.services.drive.Drive;
import com.google.api.services.drive.model.About;
import com.google.api.services.drive.model.File;
import com.google.api.services.drive.model.FileList;
import com.google.api.services.drive.model.ParentReference;
import org.datanucleus.api.jpa.annotations.Extension;
import study.alex.resumedb.gae.CredentialManager;
import study.alex.resumedb.gae.ResumeBlob;
import study.alex.resumedb.gae.ResumeBlobException;
import study.alex.resumedb.gae.ResumeDbMainServlet;


import javax.persistence.*;
import java.io.*;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Created with IntelliJ IDEA.
 * User: alex
 * Date: 24.01.14
 * Time: 16:26
 * SVN $Id$
 */

@Entity
@Inheritance (strategy = InheritanceType.TABLE_PER_CLASS)
public class GoogleDriveResumeBlob implements ResumeBlob, Serializable {

    public static final String DIRECTORY = "resumes";

    private static final Logger log = Logger.getLogger(GoogleDriveResumeBlob.class.getName());

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Extension(vendorName="datanucleus", key="gae.encoded-pk", value="true")
    protected String id;

    protected String userId = null;

/*
    @OneToOne (fetch = FetchType.EAGER, mappedBy = "body")
//    @NotNull(message = "BLOB must have a owner resume")
    protected Resume resume;
*/

    @Basic
    private String blobId;

    @Basic
    private String parent;

    private String type;

    transient private Drive drive;

    public class GoogleDriveResumeBlobException extends ResumeBlobException {

        public GoogleDriveResumeBlobException(String message) {
            super(message);
        }

    }
    public class GoogleDriveNotAvailableException extends ResumeBlobException {

        public GoogleDriveNotAvailableException(String message) {
            super(message);
        }

    }


    public boolean storeBlob(byte[] body, String mimeType) throws ResumeBlobException, IOException {
        return storeBlob(body, mimeType, createRandomName("res"));
    }

    private String createRandomName(String prefix) {

        StringBuilder name = new StringBuilder(prefix);

        GregorianCalendar today = new GregorianCalendar();
        String suffix = "-" + new Random().nextInt(1000);

        name.append(today.toString());
        name.append(suffix);
        return name.toString();
    }


    public boolean storeBlob(byte[] body, String mimeType, String name) throws GoogleDriveNotAvailableException, IOException {
        if (body == null || mimeType == null || name == null || name.isEmpty()) return false;

        log.fine("Store blob: mimeType=" + mimeType + ", name=" + name + " size=" + body.length);

        CredentialManager credentialManager = CredentialManager.getInstance();

        String oldBlob = blobId;

        if (credentialManager == null) throw new GoogleDriveNotAvailableException("Credential Manager is not available");

        if (credentialManager.getStatus(userId) != CredentialManager.GoogleDiskAccessStatus.DENY) {
            Credential credential = credentialManager.get(userId);
            drive = getDriveService(credential);

            ByteArrayContent outContent = new ByteArrayContent(mimeType, body);

            File blob;
            boolean update = true;

            if (blobId == null || blobId.isEmpty()) {
                blob = new File();
                update = false;
                blob.setDescription("A resume file");
                blob.setParents(Arrays.asList(new ParentReference().setId(parent)));
            } else blob = getFileById(drive, blobId);
            blob.setTitle(name);
            blob.setMimeType(mimeType);

            if (blob == null) throw new IOException("Can't get file metadata for Id: " + blobId);

            try {
                if (update) blob = updateContent(drive, blob, outContent);
                else blob = createFile(drive, blob, outContent);
                if (blob == null) throw new IOException("Can't create file in Google Drive");
                blobId = blob.getId();
                type = mimeType;
                log.fine("File created. Id: " + blobId + ", type=" + type);
            } catch (IOException e) {
                log.fine("Error when upload file.  Id:" + ((blob == null) ? "null" : blob.getId().toString()) + "\n" + e.toString());
                blobId = oldBlob;
                return false;
            }
        }
        return true;
    }


    public byte[] getBlob() throws GoogleDriveNotAvailableException, IOException {
        if (blobId == null || blobId.isEmpty()) return null;

        log.fine("Get blob content.  Id: " + blobId);

        CredentialManager credentialManager = CredentialManager.getInstance();

        if (credentialManager == null) throw new GoogleDriveNotAvailableException("Credential Manager is not available");

        if (credentialManager.getStatus(userId) != CredentialManager.GoogleDiskAccessStatus.DENY) {
            Credential credential = credentialManager.get(userId);
            drive = getDriveService(credential);
            File file = null;
            try {
                file = drive.files().get(blobId).execute();
            } catch (GoogleJsonResponseException e) {
                log.log(Level.WARNING, "Token revoked or it is otherwise bad", e);
                if (e.getStatusCode() == 401) {
                    // The user has revoked our token or it is otherwise bad.
                    // Delete the local copy so that their next page load will recover.
                    ((ResumeDbMainServlet)ResumeDbMainServlet.getCurrent()).dropCredential();
                }
                return null;
            } catch (TokenResponseException e) {
                log.log(Level.WARNING, "Token revoked or it is otherwise bad", e);
                ((ResumeDbMainServlet)ResumeDbMainServlet.getCurrent()).dropCredential();
                return null;
            } catch (IOException e) {
                log.log(Level.WARNING, "Exception when get file metadata.  FileId " + blobId, e);
                return null;
            }
            log.fine("Trying to get content.  Download URL: " + file.getDownloadUrl());
            return getFile(drive, file);
        } else throw new GoogleDriveNotAvailableException("Google Drive is not granted");
    }

    /**
     * Download a file's content.
     *
     * @param service Drive API service instance.
     * @param file Drive File instance.
     * @return InputStream containing the file's content if successful,
     *         {@code null} otherwise.
     */
    private InputStream downloadFile(Drive service, File file) {
        if (file.getDownloadUrl() != null && file.getDownloadUrl().length() > 0) {
            try {
                HttpResponse resp =
                        service.getRequestFactory().buildGetRequest(new GenericUrl(file.getDownloadUrl()))
                                .execute();
                return resp.getContent();
            } catch (IOException e) {
                // An error occurred.
                log.fine("Open content stream error\n" + e.toString());
                return null;
            }
        } else {
            // The file doesn't have any content stored on Drive.
            log.fine("Null content");
            return null;
        }
    }

    private byte[] getFile(Drive drive, File file) {
        if (drive == null || file == null) return null;

        log.fine("Get file content.  FileId: " + file.getId());

        ByteArrayOutputStream buffer = null;
        InputStream in = downloadFile(drive, file);

        if (in == null) {
            log.fine("File content not found");
            return null;
        }

        try {

            try {
                buffer = new ByteArrayOutputStream(in.available());

                log.fine("Download size " + in.available());

                int b;
                do {
                    b = in.read();
                    if (b < 0) break;
                    buffer.write(b);
                } while (b >= 0);
            } catch (IOException e) {
                log.warning("Error when download a file.  Link: " + file.getDownloadUrl() + "\n"+ e);
                return null;
            } finally {
                in.close();
            }
        } catch (IOException e) {
            log.warning("Error when close stream: " + file.getDownloadUrl() + "\n" + e);
            return null;
        }

        log.fine("Downloaded " + buffer.size() + " bytes.");

        return buffer.toByteArray();
    }


    public String getType() {
        return type;
    }


    public boolean deleteBlob() throws ResumeBlobException, IOException {
        if (blobId == null || blobId.isEmpty()) return false;

        CredentialManager credentialManager = CredentialManager.getInstance();

        if (credentialManager == null) throw new GoogleDriveNotAvailableException("Credential Manager is not available");

        if (credentialManager.getStatus(userId) != CredentialManager.GoogleDiskAccessStatus.DENY) {
            Credential credential = credentialManager.get(userId);
            drive = getDriveService(credential);
            try {
                drive.files().delete(blobId).execute();
            } catch (GoogleJsonResponseException e) {
                log.log(Level.WARNING, "Token revoked or it is otherwise bad", e);
                if (e.getStatusCode() == 401) {
                    // The user has revoked our token or it is otherwise bad.
                    // Delete the local copy so that their next page load will recover.
                    ((ResumeDbMainServlet)ResumeDbMainServlet.getCurrent()).dropCredential();
                }
                return false;
            } catch (TokenResponseException e) {
                log.log(Level.WARNING, "Token revoked or it is otherwise bad", e);
                ((ResumeDbMainServlet)ResumeDbMainServlet.getCurrent()).dropCredential();
                return false;
            } catch (IOException e) {
                log.log(Level.WARNING, "Exception when delete file.  FileId " + blobId, e);
                return false;
            }
            blobId = null;
        } else throw new GoogleDriveNotAvailableException("Google Drive is not granted");
        return true;
    }


    public long getMaxSize() throws ResumeBlobException, IOException {
        return getMaxSize("");
    }


    public long getMaxSize(String type) throws ResumeBlobException, IOException {

        CredentialManager credentialManager = CredentialManager.getInstance();

        if (credentialManager == null) throw new GoogleDriveNotAvailableException("Credential Manager is not available");

        long max = 0;

        if (credentialManager.getStatus(userId) != CredentialManager.GoogleDiskAccessStatus.DENY) {
            Credential credential = credentialManager.get(userId);
            drive = getDriveService(credential);
            About about = drive.about().get().execute();
            List<About.MaxUploadSizes> sizesList = about.getMaxUploadSizes();
            for (About.MaxUploadSizes s : sizesList) {
                if (s.getType().equalsIgnoreCase(type)) return s.getSize();
                if (max < s.getSize()) max = s.getSize();
            }
        } else throw new GoogleDriveNotAvailableException("Google Drive is not granted");
        return max;
    }

    /**
     * Retrieve a list of File resources in some Folder (by Id).
     *
     * @param service Drive API service instance.
     * @param folderId Folder Id
     * @return List of File resources.
     * @throws java.io.IOException
     */
    private List<File> retrieveFilesInFolderById(Drive service, String folderId) throws IOException {

        List<File> result = new ArrayList<File>();
        Drive.Files.List request = service.files().list().setQ("'" + folderId + "' in parents and trashed=false");

        do {
            try {
                FileList files = request.execute();

                result.addAll(files.getItems());
                request.setPageToken(files.getNextPageToken());
            } catch (GoogleJsonResponseException e) {
                log.log(Level.WARNING, "Token revoked or it is otherwise bad", e);
                if (e.getStatusCode() == 401) {
                    // The user has revoked our token or it is otherwise bad.
                    // Delete the local copy so that their next page load will recover.
                    ((ResumeDbMainServlet)ResumeDbMainServlet.getCurrent()).dropCredential();
                }
                request.setPageToken(null);
                break;
            } catch (TokenResponseException e) {
                log.log(Level.WARNING, "Token revoked or it is otherwise bad", e);
                ((ResumeDbMainServlet)ResumeDbMainServlet.getCurrent()).dropCredential();
                request.setPageToken(null);
                break;
            } catch (IOException e) {
                log.log(Level.SEVERE, "Exception when get about info", e);
                request.setPageToken(null);
            }
        } while (request.getPageToken() != null &&
                request.getPageToken().length() > 0);

        log.fine("Got folder list (folderId " + folderId + ").  Size is " + result.size());

        return result;
    }


    /**
     * Build and return a Drive service object based on given request parameters.
     * @param credential User credentials.
     * @return Drive service object that is ready to make requests, or null if
     *         there was a problem.
     */
    private Drive getDriveService(Credential credential) {
        return new Drive.Builder(new UrlFetchTransport(), new JacksonFactory(), credential)
                .setApplicationName("ResumeDB WS").build();
    }

    public GoogleDriveResumeBlob(String userId, Resume resume) throws GoogleDriveResumeBlobException, IOException, GoogleDriveNotAvailableException {
        this.userId = userId;
//        this.resume = resume;

        if (userId == null || resume == null) throw new GoogleDriveResumeBlobException("UserId and/or Resume is not present");

        CredentialManager credentialManager = CredentialManager.getInstance();

        if (credentialManager == null) throw new GoogleDriveNotAvailableException("Credential Manager is not available");

        if (credentialManager.getStatus(userId) != CredentialManager.GoogleDiskAccessStatus.DENY) {
            Credential credential = credentialManager.get(userId);
            drive = getDriveService(credential);
            parent = getResumeDirectoryId(drive).getId();
            if (parent == null) throw new GoogleDriveResumeBlobException("Can't get Resume Directory's Id (path ROOT/" + DIRECTORY);
        } else throw new GoogleDriveNotAvailableException("Google Drive is not granted");
    }

    /**
     * Get a file's metadata by FileId.
     *
     * @param service Drive API service instance.
     * @param fileId ID of the file to get metadata for.
     * @return A File's metadata
     */
    private File getFileById(Drive service, String fileId) {

        File file = null;

        try {
            file = service.files().get(fileId).execute();

        } catch (IOException e) {
            log.severe("An error occured: " + e);
        }

        return file;
    }


    private ParentReference getResumeDirectoryId(Drive drive) throws IOException {
        if (drive == null) return null;

        List<File> root = retrieveFilesInFolderById(drive, "root");

        if (root != null) {
            if (!root.isEmpty()) {
                for (File f : root) {
                    if (f.getTitle().equals(DIRECTORY)) return new ParentReference().setId(f.getId());
                }
            }

            File newDirectory = new File()
                                    .setMimeType("application/vnd.google-apps.folder")
                                    .setTitle(DIRECTORY);

            File dir = createFile(drive, newDirectory);
            return (dir == null) ? null : new ParentReference().setId(dir.getId());
        }
        return null;
    }

    private File createFile(Drive drive, File file) throws IOException {
        if (drive != null && file != null) {
            File response = drive.files().insert(file).execute();
            return response;
        }
        return null;
    }

    private File createFile(Drive drive, File file, AbstractInputStreamContent content) throws IOException {
        if (drive != null && file != null) {
            File response = drive.files().insert(file, content).execute();
            return response;
        }
        return null;
    }

    private File updateContent(Drive drive, File file, AbstractInputStreamContent content) throws IOException {
        if (drive != null && file != null) {
            File response = drive.files().update(file.getId(), file, content).execute();
            return response;
        }
        return null;
    }


    public String getId() {
        return id;
    }

    public String getUserId() {
        return userId;
    }

    @Override
    public long getSize() throws GoogleDriveNotAvailableException, IOException {

        if (blobId == null || blobId.isEmpty()) return -1;

        CredentialManager credentialManager = CredentialManager.getInstance();

        if (credentialManager == null) throw new GoogleDriveNotAvailableException("Credential Manager is not available");

        if (credentialManager.getStatus(userId) != CredentialManager.GoogleDiskAccessStatus.DENY) {
            Credential credential = credentialManager.get(userId);
            drive = getDriveService(credential);
            File file = getFileById(drive, blobId);
            if (file == null) return -1;
            else return file.getFileSize();
        } else throw new GoogleDriveNotAvailableException("Google Drive is not granted");
    }

    @Override
    public String getPreviewURL() throws GoogleDriveNotAvailableException, IOException {
        if (blobId == null || blobId.isEmpty()) return null;

        log.fine("Get preview URL.  Id: " + blobId);

        CredentialManager credentialManager = CredentialManager.getInstance();

        if (credentialManager == null) throw new GoogleDriveNotAvailableException("Credential Manager is not available");

        if (credentialManager.getStatus(userId) != CredentialManager.GoogleDiskAccessStatus.DENY) {
            Credential credential = credentialManager.get(userId);
            drive = getDriveService(credential);
            File file = null;
            try {
                file = drive.files().get(blobId).execute();
            } catch (GoogleJsonResponseException e) {
                log.log(Level.WARNING, "Token revoked or it is otherwise bad", e);
                if (e.getStatusCode() == 401) {
                    // The user has revoked our token or it is otherwise bad.
                    // Delete the local copy so that their next page load will recover.
                    ((ResumeDbMainServlet)ResumeDbMainServlet.getCurrent()).dropCredential();
                }
                return null;
            } catch (TokenResponseException e) {
                log.log(Level.WARNING, "Token revoked or it is otherwise bad", e);
                ((ResumeDbMainServlet)ResumeDbMainServlet.getCurrent()).dropCredential();
                return null;
            } catch (IOException e) {
                log.log(Level.WARNING, "Exception when get file metadata.  FileId " + blobId, e);
                return null;
            }
            log.fine("Got preview URL: " + file.getAlternateLink());
            return file.getAlternateLink();
        } else throw new GoogleDriveNotAvailableException("Google Drive is not granted");
    }

    @Override
    public String getOpenURL() throws GoogleDriveNotAvailableException, IOException {
        if (blobId == null || blobId.isEmpty()) return null;

        log.fine("Get open URL.  Id: " + blobId);

        CredentialManager credentialManager = CredentialManager.getInstance();

        if (credentialManager == null) throw new GoogleDriveNotAvailableException("Credential Manager is not available");

        if (credentialManager.getStatus(userId) != CredentialManager.GoogleDiskAccessStatus.DENY) {
            Credential credential = credentialManager.get(userId);
            drive = getDriveService(credential);
            File file = null;
            try {
                file = drive.files().get(blobId).execute();
            } catch (GoogleJsonResponseException e) {
                log.log(Level.WARNING, "Token revoked or it is otherwise bad", e);
                if (e.getStatusCode() == 401) {
                    // The user has revoked our token or it is otherwise bad.
                    // Delete the local copy so that their next page load will recover.
                    ((ResumeDbMainServlet)ResumeDbMainServlet.getCurrent()).dropCredential();
                }
                return null;
            } catch (TokenResponseException e) {
                log.log(Level.WARNING, "Token revoked or it is otherwise bad", e);
                ((ResumeDbMainServlet)ResumeDbMainServlet.getCurrent()).dropCredential();
                return null;
            } catch (IOException e) {
                log.log(Level.WARNING, "Exception when get file metadata.  FileId " + blobId, e);
                return null;
            }
            String url = file.getDefaultOpenWithLink();
            if (url == null) url = file.getAlternateLink();
            log.fine("Got open URL: " + url);
            return url;
        } else throw new GoogleDriveNotAvailableException("Google Drive is not granted");
    }

    @Override
    public String getThumbnailLink() throws GoogleDriveNotAvailableException, IOException {
        if (blobId == null || blobId.isEmpty()) return null;

        log.fine("Get icon URL.  Id: " + blobId);

        CredentialManager credentialManager = CredentialManager.getInstance();

        if (credentialManager == null) throw new GoogleDriveNotAvailableException("Credential Manager is not available");

        if (credentialManager.getStatus(userId) != CredentialManager.GoogleDiskAccessStatus.DENY) {
            Credential credential = credentialManager.get(userId);
            drive = getDriveService(credential);
            File file = null;
            try {
                file = drive.files().get(blobId).execute();
            } catch (GoogleJsonResponseException e) {
                log.log(Level.WARNING, "Token revoked or it is otherwise bad", e);
                if (e.getStatusCode() == 401) {
                    // The user has revoked our token or it is otherwise bad.
                    // Delete the local copy so that their next page load will recover.
                    ((ResumeDbMainServlet)ResumeDbMainServlet.getCurrent()).dropCredential();
                }
                return null;
            } catch (TokenResponseException e) {
                log.log(Level.WARNING, "Token revoked or it is otherwise bad", e);
                ((ResumeDbMainServlet)ResumeDbMainServlet.getCurrent()).dropCredential();
                return null;
            } catch (IOException e) {
                log.log(Level.WARNING, "Exception when get file metadata.  FileId " + blobId, e);
                return null;
            }
            String url = file.getThumbnailLink();
            if (url == null) url = file.getIconLink();
            log.fine("Got icon URL: " + url);
            return url;
        } else throw new GoogleDriveNotAvailableException("Google Drive is not granted");
    }
}
